/*
 * This file is Copyright 2002-2006 Loyola University Chicago,
 * Department of Computer Science and the Emerging Technologies Laboratory.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License via
 *    http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.citep.db.hibernate.sessions;

import java.math.BigDecimal;
import java.sql.SQLException;
import java.util.List;
import java.util.Set;
import java.util.Date;
import java.util.Iterator;

import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.criterion.Order;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

import com.citep.business.*;
import com.citep.business.sessions.AccountSession;
import com.citep.business.sessions.InvalidTransactionStateException;
import com.citep.business.sessions.TransactionSession;
import com.citep.business.sessions.InvalidRecordException;
import com.citep.db.hibernate.*;

public class TransactionSessionImpl extends HibernateDaoSupport implements TransactionSession {

	private AccountSession accountSession;
	
	public void setAccountSession(AccountSession accountSession) {
		this.accountSession = accountSession;
	}
	
	public Transaction getTransaction(int id) {
		return (Transaction)getHibernateTemplate().load(TransactionDAO.class, id);
	}

	public Transaction createTransaction(Transaction t) {
		getHibernateTemplate().save(t);
		return t;
	}

	public Transaction createTransaction(User u, Date transactionTime,
			String memo) {
		TransactionDAO t = new TransactionDAO();
		t.setUser(u);
		t.setMemo(memo);
		t.setTransactionTime(transactionTime);
		getHibernateTemplate().save(t);
		return t;
	}

	/* Updates the transaction. Checks each entry and updates the balance of the corresponding accounts */
	public void updateTransaction(Transaction t) throws InvalidTransactionStateException,InvalidRecordException {
		/*
		 * TODO: load a previous instance of this account. Compare for differences in entries and adjust account balances accordingly
		 */
		
		//if the transaction is a split transaction, then we must set the source to null
		Set<Entry> entries = t.getEntries();

		if(entries.size() != 2) {
			for(Entry e:entries) {
				e.setSource(null);
			}
		//otherwise we must set it to the opposite account in this transaction
		} else if(entries.size() == 2) {
			Entry [] entryArray = new Entry[2];
			entries.toArray(entryArray);
			entryArray[0].setSource(entryArray[1].getAccount());
			entryArray[1].setSource(entryArray[0].getAccount());
		}
		
		//adjust the balances for all accounts
		for(Entry e:entries) {
			Account account = e.getAccount();
			if(account == null) continue;
			adjustAccountBalance(account, e);
		}
		
		getHibernateTemplate().update(t);
	}
	
	private void adjustAccountBalance(Account account, Entry e) {
//		System.out.println("Updating balance for account: " + account);
//		System.out.println("Entry type: " + e.getType() + " amount: " + e.getAmount());
	
		if(			account.getType() == AccountTypes.ASSETS || 
					account.getType() == AccountTypes.EXPENSES) {
		
			//account balances are increased by debits, decreased by credits
			account.setCurrentBalance(account.getCurrentBalance().add(e.getAmount()));
			getHibernateTemplate().update(account);
		
		} else if(	account.getType() == AccountTypes.INCOME ||
					account.getType() == AccountTypes.LIABILITY ||
					account.getType() == AccountTypes.EQUITY) {
	
			//account balances are decreased by debits, increased by credits
			account.setCurrentBalance(account.getCurrentBalance().subtract(e.getAmount()));
			getHibernateTemplate().update(account);
		}
	
		//each balance adjustment made by an entry has to propagate
		//to the parent accounts
		if(account.getParent() != null)
			adjustAccountBalance(account.getParent(), e);

	}

	public void deleteTransaction(int id) throws InvalidRecordException {
		Transaction t = getTransaction(id);
		if(t != null)
			deleteTransaction(t);
	}

	public void deleteTransaction(Transaction t) {
		
		if(t == null) return;
		
		System.out.println("-----------------"+t.getEntries());
		if(t.getEntries() != null) {
			Iterator<Entry> entries = t.getEntries().iterator();
			while(entries.hasNext()) {
				Entry e = entries.next();
				System.out.println("+++++++++++++++++" + e);
				getHibernateTemplate().delete(e);
			}
		}
		
		getHibernateTemplate().delete(t);
	}

	/*
	 * Adds an entry to a transaction. Also checks if it's a credit or debit, and switches the amount
	 * sign correspondingly.
	 * CREDIT: negative 
	 * DEBIT: positive
	 */
	protected void addEntry(Transaction t, Account account, String memo, BigDecimal amount, EntryTypes type) throws InvalidTransactionStateException {
		
		//If we receive a negative number, switch the operation and make it positive
		if(amount.doubleValue() < 0.0) {
			amount = amount.abs();
			type = (type == EntryTypes.CREDIT)?EntryTypes.DEBIT:EntryTypes.DEBIT;
		}

		//if the operation is a credit, it is represented by a negative number
		if(type == EntryTypes.CREDIT)
			amount = amount.negate();

		EntryDAO entry = new EntryDAO();

		entry.setAccount(account);
		entry.setMemo(memo);
		entry.setAmount(amount);
		entry.setTransaction(t);
		entry.setType(type);
		
		t.getEntries().add(entry);
		
		System.out.println("Transaction entries: " + t.getEntries().size());
		
	}
	
	
	public List<Transaction> getTransactions() {
		return getHibernateTemplate().find("from TransactionDAO");
	}

	public List<Transaction> getTransactions(User u) {
		return getHibernateTemplate().find("from TransactionDAO as transaction where transaction.user = ?", u);
	}

	public List<Transaction> getTransactions(Date startDate, Date endDate) {
		Date [] params = new Date [] {startDate, endDate};
		return getHibernateTemplate().find("from TransactionDAO as transaction where transactionTime >= ? and transactionTime <= ?", params);
	}

	public List<Entry> getEntries(User u) {
		User [] params = new User [] { u };
		return getHibernateTemplate().find("select entry from TransactionDAO as transaction, EntryDAO as entry where entry.transaction = transaction and transaction.user = ?", params);
	}

	public List<Entry> getEntries(Date startDate, Date endDate) {
		Date [] params = new Date [] {startDate, endDate};
		return getHibernateTemplate().find("select entry from TransactionDAO as transaction, EntryDAO as entry where entry.transaction = transaction and transaction.transactionTime >= ? and transaction.transactionTime <= ?", params);
	}

	public List<Entry> getEntries(final Account a, final int offset, final int limit) {
		return (List)getHibernateTemplate().executeFind(new HibernateCallback() {
			public Object doInHibernate(Session session) throws HibernateException,SQLException {
				Query query = session.createQuery("select entry from EntryDAO as entry where entry.account = ?").setParameter(0, a);

				if(offset > 0)
					query.setFirstResult(offset);
				
				if(limit > 0)
					query.setMaxResults(limit);
					
				return  query.list();
				
			}
		});
	}

	public List<Transaction> getTransactions(final Account a, final int offset, final int limit) {

		return (List)getHibernateTemplate().executeFind(new HibernateCallback() {
			public Object doInHibernate(Session session) throws HibernateException, SQLException {
				Query query = session.createQuery("select distinct transaction from TransactionDAO as transaction, EntryDAO as entry where entry.transaction = transaction and entry.account = ? order by transaction.transactionTime").setParameter(0, a);
				
				if(offset > 0)
					query.setFirstResult(offset);
				
				if(limit > 0)
					query.setMaxResults(limit);
					
				return query.list();

			}
		});
	}
	
	public void transfer(User user, Account source, Account dest, EntryTypes type, String memo, Date transactionDate, BigDecimal amount) {
		Transaction transaction = createTransaction(user, transactionDate, memo);

		if(source == null) {
			
			List<Account> accountList = accountSession.getAccounts("UNBALANCED");
			if(accountList.size() < 1) {
				source = accountSession.createAccount("UNBALANCED", "unbalanced transactions", AccountTypes.ASSETS, new BigDecimal("0.00"), new Date(), new Date(), true, accountSession.createAccountCategory("unbalanced", "unbalanced"));
			} else {
				source = accountList.get(0);
			}
		}
		
		addEntry(transaction, source, memo, amount, type);
		
		if(type == EntryTypes.CREDIT)
			addEntry(transaction, dest, memo, amount, EntryTypes.DEBIT);
		else
			addEntry(transaction, dest, memo, amount, EntryTypes.CREDIT);

		updateTransaction(transaction);		
	}
	
	/*
	 * Gets the account balance up to and including this transaction
	 */
	public BigDecimal getAccountBalance(Account account, Transaction t) {
		

		List result = getHibernateTemplate().find("SELECT SUM(entry.amount) FROM EntryDAO as entry, TransactionDAO as transaction WHERE entry.transaction = transaction and transaction.transactionTime <= ? and transaction.id <= ? and entry.account = ?", new Object [] {	t.getTransactionTime(),
																																																														t.getId(),
																																																														account });
		if(result.size() == 0)
			return new BigDecimal("0.00");
		
		System.out.println("AccountBalance: " + result.get(0).toString());
		
		return new BigDecimal(result.get(0).toString());
	}


	/*
	 * Gets the account balance up to and including this transaction
	 */
	public BigDecimal getAccountBalance(Account account, int entryId) {
		

		List result = getHibernateTemplate().find("SELECT SUM(entry.amount) FROM EntryDAO as entry WHERE entry.id <= ? AND entry.account = ?", new Object [] {	entryId, account });
		
		if(result.size() == 0)
			return new BigDecimal("0.00");
		
		System.out.println("AccountBalance: " + result.get(0).toString());
		
		return new BigDecimal(result.get(0).toString());
	}

	public Date getLatestTransactionTime() {
		List result = getHibernateTemplate().executeFind(new HibernateCallback() {
			public Object doInHibernate(Session session) throws HibernateException, SQLException {
				Criteria c = session.createCriteria(TransactionDAO.class);
				c.setMaxResults(1);
				c.addOrder(Order.desc("lastModified"));
				
				return c.list();
			}
		});
		

		if(result == null || result.size() < 1)
			return null;
		
		Transaction t = (Transaction)result.get(0);
		return t.getLastModified();
	}

	
	public Date getLatestTransactionTime(final Account a) {
		List result = getHibernateTemplate().executeFind(new HibernateCallback() {
			public Object doInHibernate(Session session) throws HibernateException, SQLException {
				System.out.println("-------------------------------------");
				Query query = session.createQuery("select transaction from TransactionDAO as transaction, EntryDAO as entry where entry.transaction = transaction and entry.account = ? order by transaction.lastModified desc").setParameter(0, a);
				query.setMaxResults(1);
					
				return query.list();
			}
		});

		if(result == null || result.size() < 1)
			return null;
		
		System.out.println("+++++++++++++++++++++");
		
		Transaction t = (Transaction)result.get(0);
		return t.getLastModified();
	}
}
